在 Web 專案的開發中,身分驗證(Authentication)與狀態管理是不可或缺的環節。早期常使用基於伺服器記憶體的 Session 機制,但隨著系統微服務化與前後端分離架構的普及,JWT (JSON Web Token) 因為其「無狀態(Stateless)」的特性,成為了目前的主流方案。
然而,純粹的 JWT 機制存在著安全與管理上的缺陷。為了兼顧「系統安全性」與「良好的使用者體驗」,業界通常會採用 Access Token 搭配 Refresh Token 的雙令牌(Dual Token)機制。本篇將詳細說明其運作原理與實作細節。
1. 什麼是 JWT?
JWT 是一種開放標準(RFC 7519),主要用來在各方之間以 JSON 物件的形式安全地傳遞資訊。它的核心優勢在於自包含(Self-contained),這意味著 Token 本身就包含了使用者的基本資訊與驗證所需的簽章,後端伺服器在收到 Token 時,不需要頻繁查詢資料庫就能確認該使用者的身分與權限。
JWT 的資料結構
JWT 由三個部分組成,以點號(.)分隔:Header.Payload.Signature
Header(標頭):
記錄 Token 的類型(通常是 JWT)以及使用的雜湊加密演算法(例如 HMAC SHA256 或 RSA)。
Payload(負載):
存放實際要傳遞的資料(稱為 Claims),例如
user_id、role(權限角色),以及最重要的exp(過期時間)。⚠️ 重要觀念:Payload 只是經過 Base64Url 編碼,並沒有加密。前端任何人都可以輕易解碼看到內容,因此絕對不可以把密碼、身分證字號等敏感個資放在 Payload 裡面。
Signature(簽章):
由後端伺服器保管的「私鑰(Secret Key)」加上 Header 與 Payload 組合後進行雜湊運算產生。如果有人竄改了 Payload 的內容,後端在驗證時會發現簽章對不起來,從而拒絕該請求。
2. 為什麼需要雙令牌(Dual Token)機制?
如果我們的系統只發放單一的 Access Token,在設定過期時間(exp)時會面臨兩難:
情境一:設定長效期(例如 30 天)
優點:使用者體驗好,不用頻繁登入。
致命缺點:一旦 Token 被駭客竊取(例如透過 XSS 攻擊),駭客在接下來的 30 天內都能假冒該使用者。因為 JWT 是無狀態的,後端在不大幅修改架構的前提下,很難單獨把某個發送出去的 Token 宣告作廢。
情境二:設定短效期(例如 15 分鐘)
優點:安全性高。就算 Token 被偷,駭客能利用的時間也很短。
致命缺點:使用者體驗極差。每隔 15 分鐘,正在操作系統的使用者就會被強制登出,要求重新輸入帳號密碼。
解決方案:Access Token + Refresh Token
為了在安全性與體驗之間取得平衡,我們將權限拆分成兩把鑰匙:
Access Token(存取令牌):
效期短(通常為 5 到 15 分鐘)。
權限高,每次呼叫需要授權的 API 時都必須放在 HTTP Header 的
Authorization: Bearer <token>中帶上。
Refresh Token(更新令牌):
效期長(通常為 7 天到 30 天)。
權限單一,唯一的作用就是拿去跟伺服器換取「新的 Access Token」。
平常不參與 API 請求傳輸,降低被攔截的風險。
3. 核心運作流程與時序圖
在實際的單頁式應用(SPA,如 React、Vue)中,雙令牌機制的運作通常會依賴前端的 HTTP 攔截器(Interceptor,例如 axios.interceptors) 來達成「無感刷新」。
詳細流程如下:
sequenceDiagram
autonumber
participant User as 前端應用 (Browser)
participant API as 後端 API 伺服器
participant Auth as 授權伺服器 (Auth Service)
participant DB as 資料庫 / Redis
Note over User, DB: --- 階段一:登入取得 Token ---
User->>Auth: POST /login (提交帳號密碼)
Auth->>DB: 驗證帳密
DB-->>Auth: 驗證成功
Auth-->>User: 回傳 Access Token (短期) <br/>並透過 Set-Cookie 寫入 Refresh Token (長期)
Note over User, API: --- 階段二:正常 API 請求 ---
User->>API: 攜帶 Access Token 請求資料 (GET /data)
API-->>User: 200 OK (驗證成功,回傳資料)
Note over User, API: --- 階段三:Token 過期與攔截刷新 ---
User->>API: 攜帶 Access Token 請求資料 (GET /data)
API-->>User: 401 Unauthorized (Access Token 已過期)
rect rgb(245, 245, 245)
Note right of User: 前端攔截器捕捉到 401 錯誤,暫停原請求
User->>Auth: POST /refresh (自動攜帶 Cookie 中的 Refresh Token)
Auth->>DB: 檢查 Refresh Token 是否有效且未被列入黑名單
DB-->>Auth: 狀態正常
Auth-->>User: 核發新的 Access Token
end
Note over User, API: --- 階段四:自動重試 ---
User->>API: 攔截器使用「新的 Access Token」重新發送原先失敗的請求
API-->>User: 200 OK (使用者完全沒有察覺 Token 曾經過期)
實作細節:併發請求問題 (Race Condition)
當頁面載入時,前端可能會同時發出 5 支 API 請求。如果此時 Access Token 剛好過期,這 5 支 API 都會回傳 401。
一個健壯的前端攔截器必須實作「鎖(Lock)」或「等待佇列(Queue)」機制:
第一支收到 401 的請求會觸發
/refresh動作,並把一個旗標(如isRefreshing = true)立起來。剩下四支收到 401 的請求,會被暫存到一個 Queue 陣列中等待。
等到
/refresh成功拿到新 Token 後,再一次把 Queue 裡面的這四個請求用新 Token 重發出去。這能避免對後端造成不必要的重複刷新壓力。
4. 系統安全性最佳實踐 (Best Practices)
在實作雙令牌機制時,Token 的儲存位置與生命週期管理是資安防護的重點。
A. 儲存策略:防範 XSS 與 CSRF
Access Token:
建議儲存位置:前端的記憶體變數(Memory Variable),例如 React 的 State 或是閉包(Closure)中。
原因:放在記憶體中,駭客的 XSS 腳本很難直接去讀取。缺點是使用者只要按 F5 重新整理頁面,Token 就會消失。但沒關係,因為我們有 Refresh Token,頁面載入時立刻打一次
/refresh拿回 Access Token 即可。不建議:
localStorage。因為localStorage很容易被惡意的第三方 JavaScript 套件讀取(即 XSS 攻擊)。
Refresh Token:
建議儲存位置:HttpOnly Cookie。
原因:加上
HttpOnly屬性後,瀏覽器會嚴格禁止任何 JavaScript 讀取這個 Cookie,從根本上阻斷了 XSS 竊取 Refresh Token 的可能。防禦配套:由於使用 Cookie 會有 CSRF(跨站請求偽造)的風險,發送 Refresh Token 的 Cookie 必須設定
Secure(僅限 HTTPS 傳輸)與SameSite=Strict或Lax,並且在後端加上 CORS 限制。
B. 令牌輪轉機制 (Refresh Token Rotation)
為了進一步提升安全性,目前主流建議採用「輪轉機制」。
傳統做法是 Refresh Token 一直用到過期為止;輪轉機制則是:每次前端拿 Refresh Token 去換新的 Access Token 時,後端除了給新的 Access Token,同時也會發配一把「全新的 Refresh Token」,並把舊的 Refresh Token 標記為失效。
安全效益:如果使用者的舊 Refresh Token 被駭客偷走,當駭客嘗試拿去後端換票時,後端會發現「這個舊 Token 明明已經被換過一次了,怎麼又有人拿來換?」。此時系統可以判定該帳號可能遭到入侵,從而觸發安全機制,立刻撤銷該使用者名下的所有 Token,強制雙方都必須重新輸入帳號密碼登入。
C. 黑名單與強制登出 (Revocation)
雖然 Access Token 是無狀態的,但我們必須將 Refresh Token 有狀態化(存入關聯式資料庫或 Redis 中)。
當發生以下情況時,後端應將對應的 Refresh Token 在資料庫中標記為「撤銷(Revoked)」:
使用者主動點擊「登出」。
使用者更改密碼。
管理員在後台強制將某個使用者踢下線。
一旦 Refresh Token 被撤銷,前端攔截器的自動換票就會失敗,使用者就會被順理成章地導回登入頁面,確保了系統的管理控制權。
5. 總結
導入 Access Token 與 Refresh Token 的機制,雖然增加了前後端開發的複雜度(尤其是前端攔截器的狀態管理,以及後端資料庫對 Refresh Token 的控管),但這是目前在 Web 開發領域中,少數能同時兼顧「後端系統擴展性(無狀態)」、「高度安全性(降低權限外洩風險)」以及「優良使用者體驗(無感續航)」的成熟架構方案。